import os
import os.path
import unicodedata
import re
import locale
import codecs
import subprocess
import sqlite3
import musicbrainzngs as mb
from collections import namedtuple
from subprocess import Popen, PIPE
from glob import glob

mb.set_useragent("utils","20120314")

#flacdir = "/home/darius/shared/music/cd"
flacdir = "./cd"

def dbconn() :
	conn = sqlite3.connect("library.db")
	conn.execute("pragma foreign_keys=True")
	return conn

def all_discs(conn) :
	""""Iterates through discs.
	
	Fields in records:
	1) MB discid
	2) MB releaseid
	3) Disc number (Media number)
	4) Path to audio
	"""
	
	return conn.execute("select id, release, num, path from disc")

# old method for normalising names - a bit too conservative
def normstr (value) :
	value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
	return value.strip()


# Windows 7 won't allow these characters:
# /\:*"<>?|

# Linux will not be happy about:
# ~

# new method for making acceptable as paths
#spatt = re.compile(r'[^\w\s\!\#\"'+r"\'"+r'\&\)\(\+\-,/\.\[\:\]\;\?\|]', re.U)
spatt = re.compile(r'[^\w\s\!\#'+r"\'"+r'\&\)\(\+\-,\.\[\]\;]', re.U)

def sanitise(value) :
	if value is None :
		return u""
	value = value.replace('"',"'")
	value = value.replace('<','[')
	value = value.replace('>',']')
	value = value.replace('?','')
	value = value.replace('/','-')
	value = value.replace(':',';')
	value = spatt.sub('_', value)
	return value.strip()
	

def dest_dir(release) :
	return os.path.join(sanitise(release.artist.sortName), sanitise(release.title))

def track_labels(release) :
	release_artist = release.isSingleArtistRelease() and release.artist.name
	for track in release.tracks :
		artist = release_artist or track.artist and track.artist.name	
		yield (track.title, artist)

def track_title(tracknum, track, artist, num_media=1, max_tracks_release_media=99, max_num_media=1) :
	if max(max_num_media, num_media) > 1 :
		tracknumeral = str(num_media)
	else :
		tracknumeral = ""
	tracknumeral += "%02d" % tracknum
	return "%s %s-%s.mp3" % (tracknumeral, sanitise(track), sanitise(artist))
		
def transcmd(tracknum, src, dest, title, artist, album) :
	return "flac --decode --cue=%d.1-%d.1 -c \"%s\" | lame --preset 128 --tt \"%s\" --ta \"%s\" --tl \"%s\" --tn %d - \"%s\"" % (tracknum, tracknum+1, src, title, artist, album, tracknum, dest)

def transcode(flacfile, release) :
	isSingleArtist = release.isSingleArtistRelease()
	reldestdir = dest_dir(release)
	destdir = os.path.join(mp3dir, reldestdir)
	if not os.path.exists(destdir) :
		os.makedirs(destdir)
	
	src = os.path.join(destdir, "src.flac")
	# create a link because we have had problems passing unicode filenames to shell/flac
	if not os.path.exists(src) :
		os.link(flacfile, src)

	for (i, t) in enumerate(release.tracks) :
			#if isSingleArtist:
			#	artist = normstr(release.artist.name)
			#	#title = "%02d - %s - %s.mp3" % (i+1, normstr(t.title), normstr(release.artist.name))
			#else:
			#	artist =  normstr(t.artist.name)
			
			# If track artist is the same as release artist on multiartist release,
			# musicbrainz returns t.artist.name == None.
			# If we find this, revert to release artist
			artist = (not isSingleArtist and t.artist and normstr(t.artist.name)) or normstr(release.artist.name)	
			title = "%02d - %s - %s.mp3" % (i+1, normstr(t.title), artist)
			

			dest = os.path.join(destdir, title)
			if os.path.exists(dest) :
				continue
				
			tmpdest = os.path.join(mp3dir, "tmp", title)
			print dest
			#with open(tmpdest, "wb") as sink :
		
			cmd = transcmd(i+1, src, tmpdest, normstr(t.title), artist, normstr(release.title))
			print cmd
			subprocess.check_call(cmd, shell=True)
			
			os.rename(tmpdest, dest)
			
	os.remove(src)
	return reldestdir


def transcode_wav_to_flac(wavfile, cuefile, delete_src=False, quality="best") :
	args = ["flac", "--"+quality, "--verify", "--cuesheet="+cuefile, wavfile]
	if delete_src :
		args.insert(1, "--delete-input-file")
	return subprocess.check_call(args)

def vorbis_comments_basic(release, discid, discno) :
	yield ("MUSICBRAINZ_ALBUMID", release.strip())
	yield ("MUSICBRAINZ_DISCID", discid.strip())
	yield ("DISCNUMBER", unicode(discno))

def vorbis_comments(dbconn, releaseid, discid, discno) :
	release = Release._make(dbconn.execute(
	 	"select * from mbrelease where id=?",
	 	(releaseid,)).next())
	assert(discno <= release.numdiscs)
	# the contents of these comments were chosen from
	# http://wiki.musicbrainz.org/Picard_Tag_Mapping
	for comment in vorbis_comments_basic(releaseid, discid, discno) :
		yield comment
	yield ("ALBUM", release.title)
	yield ("ALBUMARTIST", release.artist)
	yield ("ALBUMARTISTSORT", release.artistsort)
	yield ("DISCTOTAL", unicode(release.numdiscs))
	yield ("TOTALDISCS", unicode(release.numdiscs))
	
#def write_vorbis_comments(dirname, *args, **kwargs) :
#	if not os.path.exists(dirname) :
#		os.makedirs(dirname)
#	comments = vorbis_comments(*args, **kwargs)
#	for name, value in comments :
#		path = os.path.join(dirname, name)
#		f = codecs.open(path, "w", "utf-8")
#		f.write(value)
#		f.close()
#		yield (name, path)


		
#def set_flac_tags(conn, path, release, discid, discno, dryrun=False) :

def set_flac_tags(path, comments, dryrun=False) :
	from subprocess import Popen, PIPE
	#comments = vorbis_comments(conn, release, discid, discno)
	tags = u"".join(u"%s=%s\n" % tag for tag in comments)
	if dryrun :
		print tags
		return
	toks = ["metaflac", "--no-utf8-convert", "--remove-all-tags",
			"--import-tags-from=-",path]
	process = Popen(toks, stdin=PIPE)
	process.communicate(tags.encode("utf-8"))
	if process.returncode :
		raise Exception("metaflac exited with status: %s" % str(process.returncode))
	return tags

def get_flac_comments(path) :
	toks = ["metaflac", "--no-utf8-convert", "--export-tags-to=-", path]
	process = Popen(toks, stdout=PIPE)
	tags, error = process.communicate()
	if process.returncode :
		raise Exception("metaflac exited with status: %s" % str(process.returncode))
	tags = unicode(tags, "utf-8").splitlines()
	for tag in tags :
		key, val = tag.split("=",1)
		yield key, val
	
		
def get_mbrelease(release) :
	"""Downloads the annotated release information"""
	mbrelease = mb.get_release_by_id(release, ["artist-credits","media","recordings"])["release"]
        title = mbrelease["title"]
        artist = mbrelease["artist-credit-phrase"]
        artistsort = get_artistsort(mbrelease)
        ntotaldiscs = len(mbrelease["medium-list"])
	return (title, artist, artistsort, ntotaldiscs, mbrelease)

def get_artistsort(entity) :
	"""Calculate an artist-sort string from artists in a MB entity"""
        return "; ".join(a["artist"]["sort-name"] for a in entity["artist-credit"] if isinstance(a,dict))

def tracks(release) :
	"""Extract info on tracks from the MB release entity"""
        for medium in release["medium-list"] :
                mposition = medium["position"]
                format = medium.get("format", "CD")
                if format != "CD" :
                        print "%s medium: %d: %s" % (release["title"], mposition, format)
                        print "skipping medium because it is not a CD"
                for track in medium["track-list"] :
                        position = track["position"]
                        recording = track["recording"]
                        artist = recording["artist-credit-phrase"]
                        artistsort = get_artistsort(recording)
                        title = recording["title"]
                        id = recording["id"]
                        yield (id, mposition, position, title, artist, artistsort)

def store_disc_record(conn, id, release, num, path) :
	#path = os.path.relpath(path, flacdir)
	abspath = os.path.join(flacdir, path)
	if not os.path.exists(abspath) :
		raise Exception("Tried to store invalid path: ",path)
	fdir, fhead = os.path.split(path)
	conn.execute("insert into disc values (?,?,?,?)", 
		(id, release, num, fhead))

def update_mbrelease(conn, release) :
	"""Get updated metadata on a release from the web service.

	Update the relevant rows on the mbrelease and mbtrack tables."""
	title, artist, artistsort, ntotaldiscs, mbrelease = get_mbrelease(release)
        conn.execute("insert into mbrelease values (?, ?, ?, ?, ?)",
                (release, title, artist, artistsort, ntotaldiscs))
        for track in tracks(mbrelease) :
                id, mpos, pos, title, tartist, tartistsort = track
                conn.execute("insert into mbtrack values (?,?,?,?,?,?,?)",
                        (id, release, mpos, pos, title, tartist, tartistsort))
	return mbrelease

Release = namedtuple("Release", ["id", "title", "artist", "artistsort", "numdiscs"])
Track = namedtuple("Track", ["id", "release", "discnum", "num", "title", "artist", "artistsort"])
def get_disc_mbrelease_info(conn, releaseid, discnum) :
	 release = Release._make(conn.execute(
	 	"select * from mbrelease where id=?",
	 	(releaseid,)).next())
	 artists = set()
	 tracks = [Track(*t) for t in conn.execute(
	 	"""select * from mbtrack 
	 	where release=? and discnum=?
	 	order by num""",
	 	(releaseid, discnum))]
	 
	 for track in tracks :
	 	artists.add(track.artist)
	 
	 is_single_artist = len(artists)<=1
	 return release, is_single_artist, tracks 

def get_discnum_in_release(releaseid, discid) :
	release = mb.get_release_by_id(releaseid, ["media", "discids"])
	# find the disc number by matching the disc id
	for medium in release["release"]["medium-list"] :
		discids = [d["id"] for d in medium["disc-list"]]
		if discid in discids:
	        	discnum = int(medium["position"])
	                break
	else :
		raise LookupError(("Disc number not found",release,discid))
	return discnum

def find_flac_at_path(searchpath) :
	flacpaths = glob(os.path.join(searchpath, "*.flac"))
	cuepaths = glob(os.path.join(searchpath, "*.cue"))	
	return flacpaths, cuepaths

def find_flac_file(conn, releaseid, discnum) :
	path = conn.execute("select path from disc where release=? and num=?", (releaseid, discnum)).next()[0]
	searchpath = os.path.abspath(os.path.join(flacdir, path))
	return find_flac_at_path(searchpath)
	
def read_cuefile_from_flac(path) :
	# load cuesheet from flac file
	# The cuefile will contain a FILE line with the path supplied to metaflac.
	# The easiest way to obtain a simple filename is to change the working directory.
	origdir = os.getcwd()
	fldir, flacpath = os.path.split(path)
	os.chdir(fldir)
	try :
		toks = ["metaflac", "--no-utf8-convert", "--export-cuesheet-to=-", flacpath]
		process = Popen(toks, stdout=PIPE)
		cuesheet, error = process.communicate()
		if process.returncode :
			raise Exception("metaflac exited with status: %s" % str(process.returncode))
	finally :
		os.chdir(origdir)
	return unicode(cuesheet, "utf-8")
	
def add_metadata_to_cuefile(conn, releaseid, discnum, cuesheet) :
	# load release metadata
	release, is_single_artist, tracks = get_disc_mbrelease_info(conn, releaseid, discnum)
	cuesheet = iter(cuesheet.splitlines())
	# write cue file
	yield(u'PERFORMER "%s"' % release.artist)
	yield(u'ARTISTSORT "%s"' % release.artistsort)
	yield(u'TITLE "%s"' % release.title)
	#yield(u'YEAR %s' % release.getEarliestReleaseDate())
#	yield(u'REM MUSICBRAINZ_DISC_ID "%s"' % disc.id)
	yield(u'REM MUSICBRAINZ_ALBUM_ID "%s"' % release.id)
	#yield(u'REM MUSICBRAINZ_ALBUM_ARTIST_ID "%s"' % release.artist.id)

	tracks = iter(tracks)
	for cline in cuesheet :
		track_entry = cline.strip().startswith("TRACK")
		index_entry = cline.strip().startswith("INDEX")
		yield(cline.rstrip())
		if track_entry :
			# What to do about index 0 on first track - is it required?
			track = tracks.next()
			if is_single_artist:
				title = track.title
			else:
				title = track.artist + ' - ' +  track.title
				yield(u'    PERFORMER "%s"' % track.artist)
				yield(u'    ARTISTSORT "%s"' % track.artistsort)
				
			yield(u'    TITLE "%s"' % title)
			yield(u'    REM MUSICBRAINZ_TRACK_ID "%s"' % track.id)



if __name__ == "__main__" :
	print u"\n".join(vorbis_comments("9e0797d2-a1ce-4841-92d7-dac5706e0686","HOxbvfsxFQqbZbLf3L1U.t7_O4A-",1))
	write_vorbis_comments("vorbiscomments.ini", "9e0797d2-a1ce-4841-92d7-dac5706e0686", "HOxbvfsxFQqbZbLf3L1U.t7_O4A-", 1)

